package dovs;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PushbackReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dovs.display.*;
import dovs.lexer.Lexer;
import dovs.lexer.LexerException;
import dovs.node.AProgram;
import dovs.node.ASourceFile;
import dovs.node.Start;
import dovs.parser.Parser;
import dovs.parser.ParserException;
import dovs.peephole.Loader;
import dovs.peephole.PeepholeDriver;
import dovs.peephole.PeepholeException;
import dovs.peephole.node.APatternCollection;
import dovs.peephole.node.PPatternDecl;
import dovs.phases.*;
import static dovs.display.ASTDisplay.*;

/**
 * Commandline interface for the compiler.
 * <p>
 */
public class Main {
	public static String classpath;

	public static AProgram program;

	public static boolean joos1;

	public static boolean noverify;

	public static APatternCollection peephole_patterns;

	public static String peephole_filename;
	
	/**
	 * Compile a program. Exitcodes are <code>0</code> if the compilation
	 * succeeded,<br>
	 * <code>5</code> if the program contains an error, or<br>
	 * <code>10</code> if the compiler encounters an internal compiler error.
	 * 
	 * @param args
	 *            the commandline arguments given to the compiler
	 */
	public static void main(String[] args) {
		int exitcode = 5;
		try {
			List<ErrorType> errors = compile(args);
			if (errors.isEmpty()) {
				exitcode = 0;
			} else if (errors.contains(ErrorType.INTERNAL_COMPILER_ERROR)) {
				exitcode = 10;
			}
			if (exitcode == 0 && peephole_patterns != null) {
				System.out.println("Peephole pattern hitlist:");
				PeepholeDriver.printStatistics(peephole_patterns, System.out);
			}
		} catch (Throwable e) {
			e.printStackTrace();
		} finally {
			ASTDisplay.awaitDisplays();
			System.exit(exitcode);
		}
	}
	
	protected static void displayUsage() {
		System.out.println("Usage: dovs.Main [options] <source-files>");
		System.out.println("  options:");
		System.out.println("    -classpath <classpath>   Specify classpath where the compiler should search");
		System.out.println("                             for library class files.");
		System.out.println("    -cp <classpath>          As above.");
		System.out.println("    -joos1                   Run compiler in joos1 mode");
		System.out.println("    -noverify                Do not abort the compilation if some internal code");
		System.out.println("                             consistency checks fail (in the Limits phase).");
		System.out.println("                             Useful when debugging the code generation and");
		System.out.println("                             optimization phases. ");
		System.out.println("    -o <pattern-file>        Specify peephole patterns for the optimization");
		System.out.println("                             phase.");
 		System.out.println();
		System.out.println("    -Parser                  Show AST after the Parser phase");
		System.out.println("    -Weeding                 Show AST after the Weeding phase");
		System.out.println("    -Environments            Show AST after the Environments phase (if implemented)");
		System.out.println("    -TypeLinking             Show AST after the TypeLinking phase (if implemented)");
		System.out.println("    -Hierarchy               Show AST after the Hierarchy phase (if implemented)");
		System.out.println("    -Disambiguation          Show AST after the Disambiguation phase (if implemented)");
		System.out.println("    -TypeChecking            Show AST after the TypeChecking phase (if implemented)");
		System.out.println("    -ConstantFolding         Show AST after the ConstantFolding phase (if implemented)");
		System.out.println("    -Reachability            Show AST after the Reachability phase (if implemented)");
		System.out.println("    -DefiniteAssignment      Show AST after the DefiniteAssignment phase (if implemented)");
		System.out.println("    -Resources               Show AST after the Resources phase (if implemented)");
		System.out.println("    -CodeGeneration          Show AST after the CodeGeneration phase (if implemented)");
		System.out.println("    -Optimization            Show AST after the Optimization phase (if implemented)");
		System.out.println("    -Limits                  Show AST after the Limits phase (if implemented)");
		System.out.println("    -CodeEmission            Show AST after the CodeEmission phase (if implemented)");
		System.out.println();
		if (System.getProperty("JAVA13_HOME") == null) {
			System.out.println("Be sure to set the VM argument  -DJAVA13_HOME=<jre-lib-dir>  to point to a");
			System.out.println("directory such that <jre-lib-dir>/jre/lib/ contains the rt.jar file from the");
			System.out.println("Java 1.3 installation. On daimi machines this is");
			System.out.println("  -DJAVA13_HOME=/home/java/Linux-j2sdk1_3_1");
			System.out.println("and from the zipped eclipse project this is");
			System.out.println("  -DJAVA13_HOME=lib/");
		}
	}


	/**
	 * Compile a program.
	 * <p>
	 * The compiler is designed to be re-executable, that is, this method can be
	 * called multiple times with different programs and is responsible for
	 * resetting all persistent state.
	 * 
	 * @param args
	 *            the commandline arguments given to the compiler
	 * @return a list containing the types of the errors in the order they
	 *         occured during compilation
	 */
	public static List<ErrorType> compile(String[] args) throws IOException {
		List<ErrorType> errors = new LinkedList<ErrorType>();
		classpath = null;
		program = null;
		joos1 = false;
		noverify = false;
		clearDisplayPhases();
		Errors.reset();
		try {		
			program = parseProgram(args);
			showPhaseDisplay(program, Phase.PARSER);
			Errors.check();
			
			program.apply(Weeding.aspectOf());
			showPhaseDisplay(program, Phase.WEEDING);
			Errors.check();
			
			program.apply(Environments.aspectOf());
			showPhaseDisplay(program, Phase.ENVIRONMENTS);
			Errors.check();
			
			program.apply(TypeLinking.aspectOf());
			showPhaseDisplay(program, Phase.TYPE_LINKING);
			Errors.check();
			
			program.apply(Hierarchy.aspectOf());
			showPhaseDisplay(program, Phase.HIERARCHY);
			Errors.check();
			
			program.apply(Disambiguation.aspectOf());
			showPhaseDisplay(program, Phase.DISAMBIGUATION);
			Errors.check();
			
			program.apply(TypeChecking.aspectOf());
			showPhaseDisplay(program, Phase.TYPE_CHECKING);
			Errors.check();
			
			program.apply(ConstantFolding.aspectOf());
			showPhaseDisplay(program, Phase.CONSTANT_FOLDING);
			Errors.check();
			
			program.apply(Reachability.aspectOf());
			showPhaseDisplay(program, Phase.REACHABILITY);
			Errors.check();
			
			program.apply(DefiniteAssignment.aspectOf());
			showPhaseDisplay(program, Phase.DEFINITE_ASSIGNMENT);
			Errors.check();
			
			program.apply(Resources.aspectOf());
			showPhaseDisplay(program, Phase.RESOURCES);
			Errors.check();
			
			program.apply(CodeGeneration.aspectOf());
			showPhaseDisplay(program, Phase.CODE_GENERATION);
			Errors.check();
			
			program.apply(Optimization.aspectOf());
			showPhaseDisplay(program, Phase.OPTIMIZATION);
			Errors.check();
			
			program.apply(Limits.aspectOf());
			showPhaseDisplay(program, Phase.LIMITS);
			Errors.check();
			
			program.apply(CodeEmission.aspectOf());
			showPhaseDisplay(program, Phase.CODE_EMISSION);
		} catch (SourceError e) {
			System.err.println("Errors during compilation.");
			if (e.getErrors().contains(ErrorType.ARGUMENT_ERROR)) {
				displayUsage();
			}
			return e.getErrors();
		} catch (PeepholeException e) {
			PPatternDecl pattern = PeepholeDriver.current_pattern;
			if (pattern != null) {
				System.err.println("Error in peephole pattern "
						+ pattern.getName().getText() + ":");
			} else {
				System.err.println("Error in peephole patterns:");
			}
			System.err.println(e.getMessage());
			errors.add(ErrorType.PEEPHOLE);
		} catch (InternalCompilerError e) {
			System.err.println("Internal compiler error: " + e.getMessage());
			errors.add(ErrorType.INTERNAL_COMPILER_ERROR);
		}
		return errors;
	}

	private static AProgram parseProgram(String[] args) throws IOException {
		List<ASourceFile> sources = new ArrayList<ASourceFile>();
		boolean parse_classpath = false;
		boolean parse_patterns = false;
		for (String arg : args) {
			if (parse_classpath) {
				classpath = arg;
				parse_classpath = false;
			} else if (parse_patterns) {
				// Only load patterns if not already loaded
				if (peephole_filename == null || !peephole_filename.equals(arg)) {
					try {
						peephole_patterns = Loader.load(arg);
						peephole_filename = arg;
					} catch (FileNotFoundException e) {
						Errors.errorMessage(ErrorType.PEEPHOLE,
								"Peephole pattern file " + arg + " not found");
					}
				}
				parse_patterns = false;
			} else if (arg.equals("-classpath") || arg.equals("-cp")) {
				parse_classpath = true;
			} else if (arg.equals("-joos1")) {
				joos1 = true;
			} else if (arg.equals("-noverify")) {
				noverify = true;
			} else if (arg.equals("-O")) {
				parse_patterns = true;
			} else if (arg.startsWith("-")) {
				boolean recognized = false;
				for (ASTDisplay.Phase phase : ASTDisplay.Phase.values()) {
					if (arg.equals(phase.commandline_option)) {
						ASTDisplay.addDisplayPhase(phase);
						recognized = true;
						break;
					}
				}
				if (!recognized) {
					Errors.errorMessage(ErrorType.ARGUMENT_ERROR,
						"Invalid option: " + arg);
				}
			} else {
				for (File file : expandWildcards(arg)) {
					try {
						Parser parser = new Parser(new Lexer(
								new PushbackReader(
										new InputStreamReader(new FileInputStream(
												file), "ISO-8859-1"), 4096)));
						Start startsym = parser.parse();
						ASourceFile sf = startsym.getSourceFile();
						sf.setFileName(file.getPath());
						sources.add(sf);
					} catch (FileNotFoundException e) {
						Errors.errorMessage(ErrorType.FILE_OPEN_ERROR, "File "
								+ file.getPath() + " not found");
					} catch (LexerException e) {
						Errors.errorMessage(ErrorType.LEXER_EXCEPTION,
								"Syntax error at " + file.getPath() + " "
										+ e.getMessage());
					} catch (ParserException e) {
						Errors.errorMessage(ErrorType.PARSER_EXCEPTION,
								"Syntax error at " + file.getPath() + " "
										+ e.getMessage());
					}
				}
			}
		}
		if (parse_classpath) {
			Errors.errorMessage(ErrorType.ARGUMENT_ERROR,
					"Classpath argument expected");
		}
		if (parse_patterns) {
			Errors.errorMessage(ErrorType.ARGUMENT_ERROR,
					"Peephole pattern file argument expected");
		}
		Errors.check();
		if (sources.size() == 0) {
			Errors.errorMessage(ErrorType.ARGUMENT_ERROR,
					"No source files given");
		}

		if (classpath == null) {
			classpath = System.getenv("CLASSPATH");
		}
		ClassEnvironment.init(classpath);
		return new AProgram(sources);
	}
	
	/**
	 * Returns a list of all files matching the wildcard string. Supported 
	 * wildcards are
	 * <ul>
	 *   <li>'<tt>*</tt>' matches zero or more characters within a file
	 *    or directory name</li>
	 *   <li>'<tt>**</tt>' matches directories recursively. (Only allowed as the
	 *    sole directory name, i.e. <tt>foo\**\bar</tt> is allowed but 
	 *    <tt>foo\b**\ar</tt> is not.)</li>
	 * </ul>  
	 * Both <code>/</code> and <code>\\</code> file-separators can be used.
	 * 
	 * @param wildcardString	the wildcard string
	 * @return the list of matching files.
	 */
	public static List<File> expandWildcards(String wildcardString) {
		// align separator chars
		return _expandWildcards(new File(wildcardString).getPath());
	}
	
	private static List<File> _expandWildcards(String wildcardString) {
		LinkedList<File> files = new LinkedList<File>();
		int starPos = wildcardString.indexOf("*");
		int doubleStarPos = wildcardString.indexOf("**");
		if (doubleStarPos != -1 && starPos == doubleStarPos) { // ** is the first occurence
			String prefix = "";
			if (doubleStarPos != 0) {
				prefix = wildcardString.substring(0,doubleStarPos);
				if (!prefix.endsWith(File.separator)) {
					throw new IllegalArgumentException("Illegal use of ** in " + wildcardString);
				}
			}
			if (wildcardString.charAt(doubleStarPos+2) != File.separatorChar) {
				throw new IllegalArgumentException("Illegal use of ** in " + wildcardString);
			}
			String suffix = wildcardString.substring(doubleStarPos+3);
			for (File dir : matchDirs(new File(prefix))) {
				files.addAll(_expandWildcards(new File(dir, suffix).getPath()));
			}
		} else {
			if (starPos != -1) {
				int end = wildcardString.lastIndexOf(File.separator, starPos);
				int start = wildcardString.indexOf(File.separator, starPos);
				String prefix = wildcardString.substring(0, end+1);
				String suffix = "";
				String wildcard = wildcardString.substring(end+1);
				if (start != -1) {
					suffix = wildcardString.substring(start);
					wildcard = wildcardString.substring(end+1, start);
				}
				for (File file : matchWildcard(new File(prefix), wildcard, suffix.contains(File.separator))) {
					files.addAll(_expandWildcards(new File(file, suffix).getPath()));
				}
			} else {
				files.add(new File(wildcardString));
			}
		}
		return files;
	}
	
	private static List<File> matchDirs(File dir) {
		LinkedList<File> dirs = new LinkedList<File>(); 
		if (dir.isDirectory()) {
			dirs.add(dir);
			File[] subdirs = dir.listFiles(new FileFilter() {
				public boolean accept(File file) {
					return file.isDirectory();
				}
			});
			if (subdirs != null) {
				for (File subdir : subdirs) {
					dirs.addAll(matchDirs(subdir));
				}
			}
		}
		return dirs;
	}
	
	private static List<File> matchWildcard(File dir, String wildcard, final boolean isDir) {
		StringBuilder sb = new StringBuilder();
		StringTokenizer tokenizer = new StringTokenizer(wildcard, "*", true);
		while (tokenizer.hasMoreTokens()) {
			String token = tokenizer.nextToken();
			if (token.equals("*")) {
				sb.append(".*");
			} else {
				sb.append(Pattern.quote(token));
			}
		}
		final Pattern pattern = Pattern.compile(sb.toString());
		File[] files = dir.listFiles(new FileFilter() {
			public boolean accept(File file) {
				if (file.isDirectory() != isDir) {
					return false;
				}
				Matcher matcher = pattern.matcher(file.getName());
				return matcher.matches();
			}
		});
		if (files != null) {
			return Arrays.asList(files);
		}
		return Collections.emptyList();
	}
}
